﻿using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using ShevaEngine.Core.Interfaces;
using ShevaEngine.Core.Modules.Graphics;
using ShevaEngine.Core.Modules.Materials;

namespace ShevaDeferredRenderer
{
    /// <summary>
    /// The G Buffer.
    /// </summary>
    public class GBuffer
    {
        /// <summary>Viewport.</summary>
        public GraphicsViewport Viewport;

        /// <summary>Single Z-Buffer texture with 32 bits single channel precision.
        /// Equation: -DepthVS / FarPlane</summary>
        public RenderTarget2D DepthTexture { get; private set; }
        /// <summary>Normal Map in half vector 2 format (r16f g16f) and using spherical coordinates.</summary>
        public RenderTarget2D NormalTexture { get; private set; }
        /// <summary>
        /// R: Motion vector X
        /// G: Motion vector Y
        /// B: Specular Power.
        /// A: Unused... yet.
        /// </summary>
        public RenderTarget2D MotionVectorsSpecularPowerTexture { get; private set; }
        /// <summary>The shader effect.</summary>
        private Effect Effect;        
        /// <summary>Last draw call.</summary>
        private RenderingPackage LastRenderingPair;
        /// <summary>Initial draw call.</summary>
        private RenderingPackage InitialRenderingPair;
        /// <summary>Animated.</summary>
        private Boolean Animated;
        /// <summary>Matrices.</summary>
        private List<Matrix> WorldMatrices;
        /// <summary>Matrices.</summary>
        private List<Matrix> WorldViewMatrices;
        /// <summary>DrawCall key.</summary>
        private DrawCallKey DrawCallKey;
        /// <summary>DrawCall key.</summary>
        private DrawCallKey LastDrawCallKey;

        /// <summary>
        /// This shader generates a depth and normal map.
        /// It also generates a special buffer with motion vectors for motion blur and the specular power of the material.
        /// It stores the result in two textures, the normals (normalMapTexture) and the depth (depthMapTexture).
        /// The depth texture has a texture with a 32 bits single channel precision, and the normal has a half vector 2 format (r16f g16f). 
        /// The normals are store with spherical coordinates and the depth is store using the equation: -DepthVS / FarPlane.
        /// </summary>
        internal GBuffer(GraphicsViewport viewport)
        {
            this.Viewport = viewport;

            // Multisampling on normal and depth maps makes no physical sense!!
            // 32 bits single channel precision
            this.DepthTexture = DeferredRenderer.CreateRenderTarget(this.Viewport, RenderTargetSizeType.FullScreen, true);
            // Half vector 2 format (r16f g16f). Be careful, in some GPUs this surface format is changed to the RGBA1010102 format. 
            // The XBOX 360 supports it though (http://blogs.msdn.com/b/shawnhar/archive/2010/07/09/rendertarget-formats-in-xna-game-studio-4-0.aspx)
            this.NormalTexture = DeferredRenderer.CreateRenderTarget(this.Viewport, RenderTargetSizeType.FullScreen, SurfaceFormat.HalfVector2, false);            
            // R: Motion vector X
            // G: Motion vector Y
            // B: Specular Power.
            // A: Unused... yet.
            this.MotionVectorsSpecularPowerTexture = DeferredRenderer.CreateRenderTarget(this.Viewport, RenderTargetSizeType.FullScreen, SurfaceFormat.HalfVector2, false);

            this.Effect = ShevaEngine.Core.ShevaEngine.Instance.Content.Load<Effect>("Effects\\GBuffer\\GBuffer");

            this.InitialRenderingPair = new RenderingPackage();
            this.InitialRenderingPair.DrawCall = DrawCallManager.Instance.CreateIndexedPrimitivesDrawCall(
                PrimitiveType.LineList, 0, 0, 0, 0, 0);
            this.InitialRenderingPair.DrawCallKey = new DrawCallKey(UInt16.MaxValue, UInt16.MaxValue, UInt16.MaxValue, UInt16.MaxValue);
            
            this.WorldMatrices = new List<Matrix>();
            this.WorldViewMatrices = new List<Matrix>();
        }        

        /// <summary>
        /// Render the object without taking care of the illumination information.
        /// </summary>
        private void RenderObjects(RenderingPipeline pipeline)
        {
            this.Animated = false;

            this.Effect.Parameters["FarPlane"].SetValue(this.Viewport.Camera.FarPlane);

            this.LastRenderingPair = this.InitialRenderingPair;
            this.LastDrawCallKey = this.InitialRenderingPair.DrawCallKey;
            
            this.WorldMatrices.Clear();
            this.WorldViewMatrices.Clear();

            for (int i = 0; i < pipeline.ActualCount; i++)
            {
                if (pipeline.DrawCalls[i].DrawCallKey != this.LastRenderingPair.DrawCallKey ||
                    pipeline.DrawCalls[i].DrawCall != this.LastRenderingPair.DrawCall)
                {
                    if (this.WorldMatrices.Count > 0)
                        DrawCall.RenderDrawCalls(this.Effect, this.LastRenderingPair.DrawCall,
                            this.WorldMatrices.ToArray(), this.WorldViewMatrices.ToArray());

                    this.WorldMatrices.Clear();
                    this.WorldViewMatrices.Clear();

                    this.DrawCallKey = pipeline.DrawCalls[i].DrawCallKey;

                    if ((this.DrawCallKey.MaterialID != this.LastDrawCallKey.MaterialID ||
                        this.DrawCallKey.MaterialParamsID != this.LastDrawCallKey.MaterialParamsID) &&
                        this.DrawCallKey.MaterialParamsID != DrawCallKey.None)
                    {
                        GraphicsMaterial material = MaterialManager.Instance[this.DrawCallKey.MaterialID].GraphicsProfile;

                        if (material.ParametersManager[this.DrawCallKey.MaterialParamsID].IsAnimated)
                        {
                            material.ParametersManager[this.DrawCallKey.MaterialParamsID].AnimatedParameters.Set(this.Effect);

                            this.Animated = true;
                        }
                        else
                            this.Animated = false;
                    }

                    if (!MaterialManager.Instance[this.DrawCallKey.MaterialID].GraphicsProfile.LightingEnabled)
                        continue;

                    if (this.DrawCallKey.TextureState != DrawCallKey.None)
                    {
                        if (this.DrawCallKey.TextureState != this.LastDrawCallKey.TextureState)
                        {
                            GraphicsMaterial material = MaterialManager.Instance[this.DrawCallKey.MaterialID].GraphicsProfile;
                            TextureState textureState = TextureManager.Instance[this.DrawCallKey.TextureState];                            

                            this.Effect.Parameters["SpecularPower"].SetValue(material.SpecularPower);

                            if (textureState.ContainsKey("Specular"))
                            {
                                this.Effect.Parameters["SpecularMap"].SetValue(textureState["Specular"]);
                                this.Effect.Parameters["SpecularTextured"].SetValue(true);
                            }
                            else
                                this.Effect.Parameters["SpecularTextured"].SetValue(false);

                            if (textureState.ContainsKey("Normal"))
                            {
                                this.Effect.Parameters["NormalMap"].SetValue(textureState["Normal"]);
                                this.Effect.Parameters["DiffuseMap"].SetValue(textureState["Diffuse"]);

                                if (!this.Animated)
                                    this.Effect.CurrentTechnique = this.Effect.Techniques["GBufferWithNormalMap"];
                                else
                                    this.Effect.CurrentTechnique = this.Effect.Techniques["GBufferWithNormalMapAnimated"];
                            }
                            else
                                if (textureState.ContainsKey("Diffuse"))
                                {
                                    this.Effect.Parameters["DiffuseMap"].SetValue(textureState["Diffuse"]);

                                    if (!this.Animated)
                                        this.Effect.CurrentTechnique = this.Effect.Techniques["GBufferWithDiffuseMap"];
                                    else
                                        this.Effect.CurrentTechnique = this.Effect.Techniques["GBufferWithDiffuseMapAnimated"];
                                }
                                else
                                    if (!this.Animated)
                                        this.Effect.CurrentTechnique = this.Effect.Techniques["GBufferWithoutTexture"];
                                    else
                                        this.Effect.CurrentTechnique = this.Effect.Techniques["GBufferWithoutTextureAnimated"];

                            this.Effect.CurrentTechnique.Passes[0].Apply();
                        }
                    }

                    if (this.DrawCallKey.BuffersID != DrawCallKey.None)
                        GraphicsBuffersManager.Instance.SetBuffers(this.DrawCallKey.BuffersID);

                    this.Effect.Parameters["View"].SetValue(this.Viewport.Camera.ViewMatrix);
                    this.Effect.Parameters["Projection"].SetValue(this.Viewport.Camera.ProjectionMatrix);
                    this.Effect.CurrentTechnique.Passes[0].Apply();

                    this.WorldMatrices.Add(pipeline.DrawCalls[i].Matrix);
                    this.WorldViewMatrices.Add(Matrix.Transpose(Matrix.Invert(
                        pipeline.DrawCalls[i].Matrix * this.Viewport.Camera.ViewMatrix)));

                    this.LastDrawCallKey = this.DrawCallKey;
                    this.LastRenderingPair = pipeline.DrawCalls[i];
                }
                else
                {
                    this.WorldMatrices.Add(pipeline.DrawCalls[i].Matrix);
                    this.WorldViewMatrices.Add(Matrix.Transpose(Matrix.Invert(
                        pipeline.DrawCalls[i].Matrix * this.Viewport.Camera.ViewMatrix)));
                }
            }

            if (this.WorldMatrices.Count > 0)
                DrawCall.RenderDrawCalls(this.Effect, this.LastRenderingPair.DrawCall, 
                    this.WorldMatrices.ToArray(), this.WorldViewMatrices.ToArray());            
        }

        /// <summary>
        /// It generates the depth map, normal map and motion vector/specular power map of the objects given.
        /// </summary>
        public void GenerateGBuffer(RenderingPipeline renderingPipeline)
        {
            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.BlendState = BlendState.Opaque;

            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.RasterizerState = RasterizerState.CullNone;

            DepthStencilState depthStencilState = new DepthStencilState();
            depthStencilState.DepthBufferEnable = true;
            depthStencilState.DepthBufferFunction = CompareFunction.LessEqual;
            depthStencilState.DepthBufferWriteEnable = true;

            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.DepthStencilState = depthStencilState;

            // With multiple render targets the performance can be improved.
            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.SetRenderTargets(
                this.DepthTexture, this.NormalTexture, this.MotionVectorsSpecularPowerTexture);

            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.White, 1.0f, 0);

            this.RenderObjects(renderingPipeline);

            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.SetRenderTarget(null);

            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.BlendState = BlendState.AlphaBlend;
            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.RasterizerState = RasterizerState.CullNone;
        }
    }
}
